/*
 *   Copyright 2021 zzh
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */

package com.aduib.boot.common.json;

import com.aduib.boot.common.util.DateTimePattern;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.JsonSyntaxException;
import com.google.gson.LongSerializationPolicy;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * @description: GsonParser
 * @author: zzh
 * @date: 2021/9/3 13:13
 */
public class GsonParser<T> implements JsonParser {

  public static final Type MAP_STR_OBJ_TYPE = new TypeToken<Map<String, Object>>() {}.getType();
  public static final Gson GSON =
      new GsonBuilder()
          .setLongSerializationPolicy(LongSerializationPolicy.STRING)
          .serializeNulls()
          .setPrettyPrinting()
          .setDateFormat(DateTimePattern.DATE_TIME_FORMAT)
          .registerTypeAdapter(MAP_STR_OBJ_TYPE, new MapDeserializerDoubleAsIntFix())
          .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
          .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
          .create();
  private TypeToken<T> typeToken = new TypeToken<T>() {};

  /**
   * To json string.
   *
   * @param bean the bean
   * @return the string
   */
  public static String toJson(Object bean) {
    return GSON.toJson(bean);
  }

  /**
   * To json string.
   *
   * @param builder the builder
   * @param bean the bean
   * @return the string
   */
  public static String toJson(GsonBuilder builder, Object bean) {
    return builder.create().toJson(bean);
  }

  /**
   * Parse t.
   *
   * @param <T> the type parameter
   * @param json the json
   * @param clz the clz
   * @return the t
   */
  public static <T> T parse(String json, Class<T> clz) {
    return GSON.fromJson(json, clz);
  }

  /**
   * Parse t.
   *
   * @param <T> the type parameter
   * @param builder the builder
   * @param json the json
   * @param clz the clz
   * @return the t
   */
  public static <T> T parse(GsonBuilder builder, String json, Class<T> clz) {
    return builder.create().fromJson(json, clz);
  }

  /**
   * Parse t.
   *
   * @param <T> the type parameter
   * @param json the json
   * @param type the type
   * @return the t
   */
  public static <T> T parse(String json, Type type) {
    return GSON.fromJson(json, type);
  }

  /**
   * Parse t.
   *
   * @param <T> the type parameter
   * @param builder the builder
   * @param json the json
   * @param type the type
   * @return the t
   */
  public static <T> T parse(GsonBuilder builder, String json, Type type) {
    return builder.create().fromJson(json, type);
  }

  /**
   * To json bytes byte [ ].
   *
   * @param value the value
   * @return the byte [ ]
   */
  public static byte[] toJsonBytes(Object value) {
    return toJson(value).getBytes(StandardCharsets.UTF_8);
  }

  /**
   * Google Gson
   *
   * @param jsonInString
   * @return
   */
  public static final boolean isJSONValid(String jsonInString) {
    try {
      parse(jsonInString, Object.class);
      return true;
    } catch (JsonSyntaxException ex) {
      return false;
    }
  }

  @Override
  public boolean validate(String jsonString) {
    return isJSONValid(jsonString);
  }

  @Override
  public <T> T read(String jsonString, TypeCompositeToJson type) {
    Class<?> elementClass = (Class<?>) type.getElementType();
    Class<?> containerClass = (Class<?>) type.getContainerType();
    T t = null;
    if (Objects.nonNull(type.getContainerType())) {
      t = parse(jsonString, TypeToken.getParameterized(containerClass, elementClass).getType());
    } else if (Objects.isNull(containerClass) && elementClass.isArray()) {
      t = (T) parse(jsonString, elementClass);
    } else {
      t = (T) parse(jsonString, elementClass);
    }
    return t;
  }

  @Override
  public <T> String write(T object) {
    return toJson(object);
  }

  /** 处理LocalDate的序列化与反序列化 */
  public static final class LocalDateAdapter
      implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> {

    @Override
    public JsonElement serialize(LocalDate date, Type typeOfSrc, JsonSerializationContext context) {
      return new JsonPrimitive(date.format(DateTimeFormatter.ISO_LOCAL_DATE));
    }

    @Override
    public LocalDate deserialize(JsonElement element, Type type, JsonDeserializationContext context)
        throws JsonParseException {
      String timestamp = element.getAsJsonPrimitive().getAsString();
      return LocalDate.parse(timestamp, DateTimeFormatter.ISO_LOCAL_DATE);
    }
  }

  /** 处理LocalDateTime序列化与反序列化 */
  public static final class LocalDateTimeAdapter
      implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {

    @Override
    public JsonElement serialize(
        LocalDateTime date, Type typeOfSrc, JsonSerializationContext context) {
      return new JsonPrimitive(
          date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.CHINA)));
    }

    @Override
    public LocalDateTime deserialize(
        JsonElement element, Type type, JsonDeserializationContext context)
        throws JsonParseException {
      String timestamp = element.getAsJsonPrimitive().getAsString();
      return LocalDateTime.parse(
          timestamp, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.CHINA));
    }
  }

  /**
   * https://gist.github.com/xingstarx/5ddc14ff6ca68ba4097815c90d1c47cc
   *
   * <p>https://stackoverflow.com/questions/36508323/how-can-i-prevent-gson-from-converting-integers-to-doubles/36529534#36529534
   *
   * <p>
   *
   * <p>解决json数据转换为map结构的时候，会出现int变成double的问题
   */
  public static final class MapDeserializerDoubleAsIntFix
      implements JsonDeserializer<Map<String, Object>> {

    @SuppressWarnings("unchecked")
    @Override
    public Map<String, Object> deserialize(
        JsonElement element, Type type, JsonDeserializationContext context)
        throws JsonParseException {
      return (Map<String, Object>) read(element);
    }

    private Object read(JsonElement in) {
      if (in.isJsonArray()) {
        List<Object> list = new ArrayList<>();
        JsonArray arr = in.getAsJsonArray();
        for (JsonElement anArr : arr) {
          list.add(read(anArr));
        }
        return list;
      } else if (in.isJsonObject()) {
        Map<String, Object> map = new LinkedTreeMap<>();
        JsonObject obj = in.getAsJsonObject();
        Set<Map.Entry<String, JsonElement>> entitySet = obj.entrySet();
        for (Map.Entry<String, JsonElement> entry : entitySet) {
          map.put(entry.getKey(), read(entry.getValue()));
        }
        return map;
      } else if (in.isJsonPrimitive()) {
        JsonPrimitive prim = in.getAsJsonPrimitive();
        if (prim.isBoolean()) {
          return prim.getAsBoolean();
        } else if (prim.isString()) {
          return prim.getAsString();
        } else if (prim.isNumber()) {
          Number num = prim.getAsNumber();
          // here you can handle double int/long values
          // and return any type you want
          // this solution will transform 3.0 float to long values
          if (Math.ceil(num.doubleValue()) == num.longValue()) {
            return num.longValue();
          } else {
            return num.doubleValue();
          }
        }
      }
      return null;
    }
  }
}
